Primeros pasos en el análisis textual en RStudio

Romina De León

romideleon@gmail.com

Durante esta ejercitación, utilizaremos los materiales de la unidad Nº 1, así como la teoría correspondiente.

El corpus que les presento está relacionado con los módulos anteriores de la Diplomatura y se encuentra disponible en el campus. Este corpus está compuesto por textos escritos por viajeros, cronistas, naturistas y antropólogos que realizaron viajes por la región actual de Argentina y algunos países limítrofes, entre los siglos XVI y XVIII. Los autores incluidos son: Félix de Azará, Ruy Díaz de Guzmán, Acarette du Biscay, William H. Hudson, Pero Hernández, Francisco Pascasio Moreno, Ulrico Schmidl, Antonio Pigafetta y Antonio de Viedma.

Cada texto ha sido extraído de diferentes repositorios, convertido a formato de texto plano .txt y limpiado para su utilización en esta unidad y en la siguiente. En la carpeta comprimida encontrarán cada uno de los textos nombrados con el apellido del autor, seguido de un guion bajo _ y una sugerencia del nombre del documento, con la extensión ‘.txt’.

rm(list = ls()) #limpiamos el entorno

getwd() #nos ubicamos en nuestra carpeta

Primero paso: Puedes descargar y descomprimir como lo realizas habitualmente la carpeta directamente del campus, o sino mediante el comando download.file descargarás el archivo *.zip con la url entre comillas, luego indicarás el nombre del archivo y finalmente con el parámetro wget se indica el método utilizado para realizar la descarga, aclaración en Windows debes cambiarlo por wininet. Luego, descomprime el archivo con el comando unzip. A continuación ingresa a tu carpeta de trabajo.

#download.file("https://github.com/rominicky/materiales/raw/refs/heads/main/assets/corpus.zip", destfile="corpus.zip", "wget") # Aclaración para Windows reemplazar 'wget' por "wininet"

#unzip("corpus.zip") #descomprimir la carpeta

setwd('./corpus') # Ingresa a la carpeta que se ha creado, recuerda que puedes incluso crear otra carpeta para datos de entrada (input) y datos de salida (output) para cargar y guardar tus archivos

El paso a seguir es llamar a las librerías que utilizaremos:

library("tidytext", "tidyverse")

Una vez instalados estos requerimientos, llamarás a la función list.file para corroborar que los archivos necesarios se encuentran en la carpeta donde estás trabajando.

list.files('./corpus')

En un segundo paso, conformarás una nueva lista, a la que puedes llamar archivos, cuyos elementos serán los textos planos dentro de la carpeta corpus, para ello, utilizarás, nuevamente, la función list.files, y le indicarás que tipo de archivo debe contener, txt, con la declaración pattern. Recuerda estar trabajando en la carpeta donde se encuentran tus archivos, sino deberás incorporar un declaración que indique la ubicación, path =, por ejemplo: list.files(path = "corpus", pattern = "\\.txt$").

archivos <- list.files(path = "./corpus",pattern = "\\.txt$")

archivos #corroborá que la lista está correcta
## [1] "Azara_Descripcion.txt"      "DiazDeGuzman_ArgManus.txt" 
## [3] "DuBiscay_RelDeUnViaje.txt"  "Hudson_DiasDeOcio.txt"     
## [5] "PHernandez_RelCosas.txt"    "Pigafetta_PrimerViaje.txt" 
## [7] "PMoreno_ViajePatagonia.txt" "Schmidl_ViajeAlRdP.txt"    
## [9] "Viedma_Diario.txt"
class(archivos) #la función 'class()' devuelve el tipo de objeto, en este caso podrás ver que se trata de una lista de caracteres
## [1] "character"
length(archivos) #la función 'length()' permite obtener el tamaño del objeto que se incluye dentro de los paréntesis
## [1] 9

Tercer paso, será para facilitar la visualización de cada texto en los siguientes pasos, por lo cual, se eliminará lo que no es necesario del nombre de cada archivo. Para ello, utilizarás la función gsub, de gran utilidad para limpieza de datos que está incluida por defecto en R. Los parámetros indicarán, según orden de escritura, caracteres que se eliminarán (\\. será para indicar que se busca un . y no otro carácter según las reglas de expresión regular), los que se agregarán (las doble comillas indican que no se agregará nada), y finalmente, con perl, se determina que se utilicen las reglas de expresiones regulares3.

textos_archivo <- gsub("\\.txt", "", archivos, perl = TRUE)

#Ahora llama a la lista para corroborar que se ha realizado la limpieza

textos_archivo
## [1] "Azara_Descripcion"      "DiazDeGuzman_ArgManus"  "DuBiscay_RelDeUnViaje" 
## [4] "Hudson_DiasDeOcio"      "PHernandez_RelCosas"    "Pigafetta_PrimerViaje" 
## [7] "PMoreno_ViajePatagonia" "Schmidl_ViajeAlRdP"     "Viedma_Diario"

Cuarto paso, con la lista de textos acorde para continuar el ejercicio, procede a armar un tibble, este objeto es muy similar a los data.frame presentados en la unidad anterior, pues es rectangular, organizado en filas y columnas, y pertenece al paquete tidyverse. Existen tres diferencias entre los data.frame y los tibble, la primera es como se muestran en consola; segunda, los tibble eliminan por defecto rownames, e incluso no se recomienda su uso, para evitar problemas de compatibilidad principalmente con bases de datos SQL; por último, no convierte a las string (cadenas de caracteres) en factores. Igualmente, ambos objetos son intercambiables4.

Armarás el tibble con tres columnas, la primera y última tendrán caracteres dentro de sus variables, y parrafo será numérica, como se muestra a continuación:

library(dplyr)
## 
## Adjuntando el paquete: 'dplyr'
## The following objects are masked from 'package:stats':
## 
##     filter, lag
## The following objects are masked from 'package:base':
## 
##     intersect, setdiff, setequal, union
mensajes <- tibble(textos_archivo = character(),
                   parrafo = numeric(),
                   texto = character())

#Si llamas a "mensajes" comprobarás que figura como vacío, esto es porque aún no se ha completado con datos, ello lo realizarás en el próximo paso.

mensajes

Para poder completar el tibble mensajes que has creado, realizarás una iteración con el bucle for, que permite repetir instrucciones, evaluando el mismo código para cada elemento de un vector o lista.

En este ejercicio, se le indica que comience la iteración en 1 hasta el tamaño (length) de archivos. Luego, armará en el objeto corpus lo que leerá con la función read_lines, allí se indicará que con la función paste junte los textos dentro de tu carpeta de trabajo, importante añadir esa dirección, con el contenido de archivos[i] , cuyo valor cambiará en cada repetición del bucle. El parámetro sep = será para indicar que los pegue con “/“ .

Paso siguiente, convertir lo que acaba de leer en una nueva tabla, otra tibble llamada temporal, tal como hiciste con la anterior de mensajes, indicando que complete con cada texto. La primer columna, parrafo indicará el número de párrafos de cada texto, se lo adjudicará con la función seq_along que contará hasta el número máximo en el objeto corpus_total y por último, en texto se agregará cada párrafo. Para finalizar la repetición, indicarás que en mensajes se sume la tabla temporal, mediante la función bind_rows.

library(readr)
#Iteración 

for (i in 1:length(archivos)){
  corpus_total <- read_lines(paste('./corpus',
                               archivos[i],
                               sep = "/"))
  temporal <- tibble(textos_archivo = textos_archivo[i],
                     parrafo = seq_along(corpus_total),
                     texto = corpus_total)
  mensajes <- bind_rows(mensajes, temporal)
} 

mensajes #corrobora que se creó correctamente la tabla

Este sexto paso, será para proceder con la tokenización del corpus, es decir extraer sus unidades de análisis. Para ello, se utilizará la función unnest_tokens que se encuentra dentro del paquete tidytext; se indicará que a cada fila de parrafo, lo separe a nivel de palabra según la columna texto.

mensajes_palabras estará formado por tres columnas, una que indicará el texto, la otra el número de párrafo y finalmente, las palabras de cada uno, pero separadas. Recuerda que el operador pipe (%>%) fue presentado en la primera unidad.

mensajes_palabras <- mensajes %>% 
  unnest_tokens(palabra, texto)


mensajes_palabras

Como siguiente paso, se eliminarán las palabras vacías, que no serán útil en este ejercicio, pero si pueden servir para otros análisis; asimismo, lo realizarás para continuar con la parte de ejercitación que solicitaré como extra. Para ello, se armará una una tabla con las palabras que se obtienen de la función get_stopwords que forma parte del paquete tidytext, y se aclarará que se utilice en español.

stopwords1 <- get_stopwords("es")

stopwords1

Paso siguiente, se le indicará que cambie la columna word por palabra, mediante la función rename, que forma parte del paquete dplyr5, que a su vez pertenece a tidyverse, es útil para cambiar el nombre de variables u objetos. Cuestión que debe considerarse al momento de renombrar, es que primero debe escribirse el nuevo término, y a continuación el que se va a cambiar. Este paso es importante para poder eliminar las palabras vacías de mensajes_palabras.

stopwords1 <- stopwords1 %>%
rename(palabra = word)

stopwords1

Ahora, para poder obtener una nueva lista con palabras vacías excluidas, se usará la función anti_join que compara las columnas con el mismo nombre, en este caso palabra y elimina los elementos que coinciden. Cuando finalice la ejecución, aparece un mensaje que significa que las dos listas tienen iguales elementos en esas columnas, en caso contraria, devolverá mensaje de error.

mensajes_sin_vacias <- mensajes_palabras %>%
anti_join(stopwords1)
## Joining with `by = join_by(palabra)`

Como paso siguiente, se procederá a la limpieza de la lista mensajes_palabras y mensajes_sin_vacias. Para ello, se eliminarán dígitos, palabras con menos de tres caracteres, celdas sin caracteres y las que contengan puntos, todo ello se realizará en la columna palabra. Por lo cual, se utilizarán las siguientes funciones:

Repetirás las limpieza en las dos listas, recuerda guardar las que no fueron limpias, adjudicando nuevos nombres a las que si se hayan limpiado.

library(stringr)

mensajes_pal_limpio <- mensajes_palabras %>%
mutate(palabra = str_remove_all(palabra, "\\d+")) %>%
filter(nchar(palabra) > 3) %>% 
filter(palabra != "") %>%   
filter(palabra != ".") %>%   
filter(palabra != "n°")


mensajes_pal_limpio

Indica nuevo nombre a esta tabla que se ha limpiado. Esta nueva tabla será útil para realizar la parte de ejercitación extra.

mensajes_limpio <- mensajes_sin_vacias %>%
mutate(palabra = str_remove_all(palabra, "\\d+")) %>%
filter(nchar(palabra) > 3) %>% 
filter(palabra != "") %>%   
filter(palabra != ".") %>%   
filter(palabra != "n°")


mensajes_limpio

En el paso que sigue, se dará inicio a los análisis de frecuencia y gráficos de los textos del corpus. Para lo cual, primero se contará las palabras que se encuentran dentro de la columna palabra en mensajes_pal_limpio, por medio de la función count, que también pertenece al paquete tidyverse, y presentará la frecuencia absoluta de cada una, o sea la cantidad de veces que se repite.

mensajes_pal_limpio %>% 
  count(palabra, sort = TRUE)

Como la frecuencia absoluta no representa ningún dato novedoso, se calculará el promedio de palabras por texto, que no han sido procesadas con la limpieza de datos, pudiéndose calcular entre el resultado del número de columnas obtenidos con la función nrow y el tamaño de la lista archivos, calculado en los primeros pasos.

nrow(mensajes_palabras) / length(archivos)
## [1] 49276.67

Ese dato solo indicará un promedio, por lo cual, a continuación se calculará la frecuencia relativa por palabras y será adjudica como una nueva columna, dentro del tibble limpio: mensajes_pal_limpio, que se denominará relativa. Por lo cual tendremos cuatro columnas, la del término, su frecuencia absoluta, la relativa y el TF-IDF. Por ello armaremos un nuevo tibble denominado, mensajes_frecuencias:

# Calculo de la frecuencia absoluta, relativa y TF-IDF

mensajes_frec_limpio <- mensajes_pal_limpio %>%
  count(palabra, sort = TRUE) %>%
  mutate(
    relativa = n / sum(n),            # Frecuencia relativa
    tf_idf = relativa * log(nrow(mensajes_pal_limpio) / n)  # Cálculo TF-IDF
  )

# Mostrar resultado con la nueva columna de TF-IDF
mensajes_frec_limpio

También, podrías pedir que devuelva la frecuencia por cada texto, para ello, se deberá agrupar por cada uno mediante la función group_by, del paquete tidyverse, únicamente indicando que debe considerar la lista de textos, antes preparada; luego del cálculo de la frecuencia absoluta, debe crearse una nueva columna para la frecuencia relativa, y finalmente que desagrupe los datos, con ungroup(). Todo ello, será guardado en otra tabla frecuencia_text_arch:

frecuencia_text_arch <- mensajes_pal_limpio %>%
           group_by(textos_archivo) %>%
           count(palabra, sort = T) %>%
           mutate(
    relativa = n / sum(n),            # Frecuencia relativa
    tf_idf = relativa * log(nrow(mensajes_pal_limpio) / n)  # Cálculo TF-IDF
  ) %>%
           ungroup()

frecuencia_text_arch

Ahora, para visualizar estos datos calculados realizarás un gráfico, donde se presentará la frecuencia por cada texto, con la función ggplot, que fue convocada al principio de la práctica con el paquete tidyverse, se le indicará que se graficará en barras, mediante el parámetro geom_bar, luego se indicará con aes, el contenido de los eje X e Y, en ese orden; y por último, con stat, que tipo de transformación estadísticas tendrán los datos.

library(ggplot2)
  mensajes_pal_limpio %>%
             group_by(textos_archivo) %>%
            count(palabra, sort = T) %>%
             ggplot() +
             geom_bar(aes(textos_archivo,
                          n, fill = palabra), #la variación de color es por palabras si cambiamos palabras, por textos_archivos, será por cada documento
                      stat = 'identity', width = 0.7) + #indico el tamaño del gráfico
    theme(legend.position = "none")  +
    theme(axis.text.x = element_text(angle = 45, hjust = 1))

#ggsave("mensajes_pal_limpio.png")

Otro gráfico que se puede realizar es observando las palabras más frecuentes de los textos, para ello primero, se llamará a mensajes_pal_limpio, se pedirá que cuente las ocurrencias, ordenadas de mayor a menor; en la tercera línea se indicará que filtre las ocurrencias mayores a 750, pues se han eliminado las palabras vacías. Como se explicó antes, al crear una nueva tabla interna se debe indicar con mutate, la nueva columna, que almacenará los nuevos datos. Luego se indicarán los parámetros del gráfico. Aclaración, ggplot une sus parámetros con +; el argumento fill hará que cada palabra tenga un color determinado, stat indicará que las barras tendrán la altura según n. Las líneas siguientes presentarán parámetros para título, ubicación de leyenda y etiquetas en los ejes; y con coord_flip() se asegura que el eje Y se imprima de forma horizontal.

mensajes_pal_limpio %>%
  count(palabra, sort = T) %>%
  filter(n > 750) %>%
  mutate(palabra = reorder(palabra, n)) %>%
  ggplot(aes(x = palabra, y = n, fill = palabra)) +
  geom_bar(stat="identity", width = 1) +
  theme_minimal() +
  theme(legend.position = "none") +
  ylab("Número de veces que aparecen") +
  xlab(NULL) +
  ggtitle("Discurso-DNU") +
  coord_flip()

ggsave("mensajes_palabras.png")
## Saving 7 x 5 in image

Otra utilidad, que ha sido explicada en la parte teórica de la Unidad 2, es la Ley de Zipf, por lo cual, se reorganizarán los parámetros de ggplot para poder apreciar la relación entre cada palabra y su frecuencia.

Para ello, primero se armará una tabla con mensajes_palabras, con columnas con frecuencia absoluta = n y frecuencia relativa, sumada una última columna, que indicará el orden de palabras en forma descendente, o el número de filas, llamada Clasificacion.

mensajes_frecuencias <- mensajes_palabras %>%
    count (palabra, sort = TRUE) %>%
    mutate( relativa = n / sum(n),            # Frecuencia relativa
      tf_idf = relativa * log(nrow(mensajes_pal_limpio) / n)  # Cálculo TF-IDF
    ) %>% 
     mutate(Clasificacion = row_number())
  
  mensajes_frecuencias

Para realizar el gráfico con dichos datos, se utilizará la tabla anterior, y se indicará en la tercera línea, mediante geom_line la forma en que va a realizar la línea del gráfico; y en las últimas dos, se indicará que en ambos ejes se usará la escala logarítimica.

mensajes_frecuencias %>%  
    ggplot(aes(Clasificacion, relativa, color = n)) + 
    geom_line(size = 1.5, alpha = 0.8, show.legend = FALSE) +  # Línea más delgada y con transparencia ajustada
    scale_x_log10() +  # Escala logarítmica en el eje X
    scale_y_log10() +  # Escala logarítmica en el eje Y
    scale_color_gradient(low = "lightblue", high = "lightpink") +  # Gradiente de color
    labs(
      title = "Comprobación Ley de Zipf",
      x = "Clasificación",
      y = "Frecuencia Relativa"
    ) +
    theme_minimal()
## Warning: Using `size` aesthetic for lines was deprecated in ggplot2 3.4.0.
  ## ℹ Please use `linewidth` instead.
  ## This warning is displayed once every 8 hours.
  ## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
  ## generated.

Recordemos también otra ley que hemos trabajado en la parte teórica, la ley de Heaps, para lo cual primero calcularemos el total_palabras, es decir, el número total de palabras procesadas hasta ese punto en el texto y el vocabulario, que representará el conteo acumulativo de palabras únicas encontradas en el texto. Esto nos permitirá analizar cómo crece el vocabulario a medida que se procesan más palabras en cada texto, posteriormente visualizaremos los datos mediante ggplot2.

# Calcular el crecimiento del vocabulario
  crecimiento_vocabulario <- mensajes_palabras %>%
    # Excluir el texto "Azara_Descripcion"
   # filter(!textos_archivo %in% c("Azara_Descripcion", "PMoreno_ViajePatagonia")) %>%
    # Contar el total de palabras procesadas
    group_by(textos_archivo) %>%
    mutate(total_palabras = row_number()) %>%
    # Contar el vocabulario único a medida que se procesan las palabras
    mutate(vocabulario = cumsum(!duplicated(palabra))) %>%
    ungroup()  # Desagrupar para evitar problemas con ggplot

Ahora, generaremos una gráfica que muestre cómo crece el vocabulario (número de palabras únicas) en función del número total de palabras procesadas para cada uno de los textos que hemos analizado. Podremos observar líneas de diferentes colores que representan los diferentes textos, y puntos, que no son muy útiles por la cantidad de datos, y deberían añadir mayor claridad a la visualización del crecimiento del vocabulario. La gráfica es una representación visual útil para observar la Ley de Heaps y cómo el vocabulario se expande a medida que se procesan más palabras.

# Graficar la Ley de Heaps
  ggplot(crecimiento_vocabulario, aes(x = total_palabras, y = vocabulario, color = textos_archivo)) +
    geom_line(aes(group = textos_archivo), size = 0.1) +  # Línea para cada texto
    geom_point(size = 1.5) +  # puntos, poco útiles, porque no agregan mayor claridad, he cambiado el tamaño de puntos y de líneas, pero no varía demasiado
    labs(
      title = "Comprobación de la ley de Heaps",
      x = "Total de palabras",
      y = "Vocabulario o palabras únicas"
    ) +
    theme_minimal() +
    scale_color_discrete(name = "Textos")  # Agregar leyenda para los textos

Ejercitación

Notas

  1. Para más información sobre tidyverse acceder a https://www.tidyverse.org/learn/, y para más información sobre tidytext en https://cran.r-project.org/web/packages/tidytext/tidytext.pdf.↩︎

  2. Pueden encontrar más información sobre la librería en: https://ggplot2.tidyverse.org/.↩︎

  3. Para más información sobre expresiones regulares, pueden acceder a: https://indexingdata.com/blog/analitica-web/guia-basica-expresiones-regulares/, https://regex101.com/ este sitio es para ejercitar nuestras regex y posee en el cuadrante inferior derecho una guía rápida, y también en https://www.rexegg.com/regex-quickstart.php.↩︎

  4. Puedes acceder a más información sobre tibble en https://cran.r-project.org/web/packages/tibble/vignettes/tibble.html; y sobre data.frame en https://www.rdocumentation.org/packages/base/versions/3.6.2/topics/data.frame. Los tibbles son una versión mejorada de los data.frames, pues son diseñados para facilitar la manipulación y visualización de datos en R. Si bien se pueden utilizar ambos, los tibbles son especialmente compatibles con dplyr y tidyverse.↩︎

  5. Para más información sobre la librería dplyr acceder a https://dplyr.tidyverse.org/.↩︎